/*
*	JSMock 1.1, a mock object library for JavaScript 
* Copyright (C) 2006 Justin DeWind 
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

function Mock() {
	/**
		@private
	*/
	this.calls = new Array();
}

Mock.prototype = {
	calls: null,

	/**
		@returns {Mock}
	*/
	expect: function() { 
		this.__recording = true; 
		return this; 
	},

	addMockMethod: function(method) {},	

	/**
		@private
	*/
	__recording: false
} 

function MockControl() {
	/**
		@private
	*/
	this.__expectationMatcher = new ExpectationMatcher();
	/**
		@private
	*/
	this.__lastMock = null;
	/**
		@private
	*/
	this.__lastCallName = null;
} 

MockControl.prototype = {

	/**
		@returns {Mock}
	*/
	createMock: function(object) {
		var mock = new Mock();

		if(object != null) {

			if( typeof(object) == 'function' ) {
				this.__createMethods(object, mock);
				this.__createMethods(new object(), mock);
			} 
			else if( typeof(object) == 'object') {
				this.__createMethods(object, mock);
			} 
			else {
				throw new Error("Cannot mock out a " + typeof(object));
			}

		}

		control = this;
		mock.addMockMethod = function(method) { control.__createMethod(control, mock, method); }

		return mock;
	},
	
	andReturn: function(returnValue) {
		this.__verifyLastMockNotNull("Cannot set return value without an expectation");
		this.__initializeReturnExpectationForMock();	
		this.__lastMock.calls[this.__lastCallName].push( function() { return returnValue; });
	},
	
	andThrow: function(throwMsg) {
		this.__verifyLastMockNotNull("Cannot throw error without an expectation");
		this.__initializeReturnExpectationForMock();	
		this.__lastMock.calls[this.__lastCallName].push( function() { throw new Error(throwMsg); });
	},

	andStub: function(block) {
		this.__verifyLastMockNotNull("Cannot stub without an expectation");
		if( typeof(block) != 'function') {
			throw new Error("Stub must be a function");
		}
		this.__initializeReturnExpectationForMock();	
		var parseArgs = this.__parseArgs;
		this.__lastMock.calls[this.__lastCallName].push( function() { parsedArguments = parseArgs(arguments); eval("block(" + parsedArguments + ")"); });
	},

	verify: function() {
		if(!this.__expectationMatcher.matches())
		{
			discrepancy = this.__expectationMatcher.discrepancy();
			message = discrepancy.message;
			method = discrepancy.behavior.method
			formattedArgs = ArgumentFormatter.format(discrepancy.behavior.methodArguments);
			this.__expectationMatcher.reset();
			throw new Error(message + ": " + method + "(" + formattedArgs + ")");	
		}

	},

	/**
		@private
	*/
	__createMethods: function(object, mock) {
		for( property in object )	{
			if( this.__isPublicMethod(object, property) ) {
				this.__createMethod( this, mock, property );
			}
		}
	},

	/**
		@private
	*/
	__createMethod: function(control, mock, method) {
		mock[method] = 
			function() {
				if( mock.__recording ) {
						control.__lastMock = mock;
						control.__lastCallName = method;
						control.__expectationMatcher.addExpectedMethodCall( mock, method, arguments );
						mock.__recording = false;
						return control;
					} 
					else {
						control.__expectationMatcher.addActualMethodCall( mock, method, arguments );
						if( mock.calls[method] != null) {
							returnValue = mock.calls[method].shift();
							if( typeof(returnValue) == 'function') {
								parsedArguments = control.__parseArgs(arguments);
								return eval("returnValue(" + parsedArguments + ")");
							}
						}
					}
			}
	},

	/**
		@private
	*/
	__isPublicMethod: function(object, property) {
		try {
			return typeof(object[property]) == 'function' && property.charAt(0) != "_";
		} catch(e) {
			return false;
		}
	},

	/**
		@private
	*/
	__verifyLastMockNotNull: function(throwMsg) {
		if(this.__lastMock == null) {
			throw new Error(throwMsg);
		}
	},

	/**
		@private
	*/
	__initializeReturnExpectationForMock: function() {
		if(typeof(this.__lastMock.calls[this.__lastCallName]) == 'undefined') {
			this.__lastMock.calls[this.__lastCallName] = new Array();
		} 
	},

	/**
		@private
	*/
	__parseArgs: function(args) {
		var argsList = "";
		for(var i = 0; i < args.length; i++) {
			argsList += 	( argsList == "" ) ? "arguments[" + i + "]" : ", arguments[" + i + "]";
		}
		return argsList;
	}
}

function ExpectationMatcher() {
	/**
		@private
	*/
	this.__expectationBehaviorList = new Array();
	/**
		@private
	*/
	this.__actualBehaviorList = new Array();
}

ExpectationMatcher.prototype = {
	addExpectedMethodCall: function(caller, method, methodArguments ) {
		this.__expectationBehaviorList.push(new InvocationBehavior(caller, method, methodArguments));
	},

	addActualMethodCall: function(caller, method, methodArguments ) {
		this.__actualBehaviorList.push(new InvocationBehavior(caller, method, methodArguments));
	},

	matches: function() {
		while(expectedBehavior = this.__expectationBehaviorList.shift()) {
			var actualBehavior = this.__actualBehaviorList.shift();

			if( typeof(actualBehavior) == "undefined" ) {
				this.__discrepancy = new Discrepancy("Expected function not called", expectedBehavior);
				return false;
			}	
			else if( expectedBehavior.method != actualBehavior.method ) {
				this.__discrepancy = new Discrepancy("Surprise call", actualBehavior);
				return false;
			}
			else if( expectedBehavior.caller != actualBehavior.caller ) {
				this.__discrepancy = new Discrepancy("Surprise call from unexpected caller", actualBehavior);
				return false;
			} 
			else if( !this.__matchArguments(expectedBehavior.methodArguments, actualBehavior.methodArguments) ) {
				this.__discrepancy = new Discrepancy("Unexpected Arguments", actualBehavior);
				return false;
			}
			
		}
		
		if( this.__actualBehaviorList.length > 0 ) {
			this.__discrepancy = new Discrepancy("Surprise call", this.__actualBehaviorList.shift());
			return false;
		} 

		return true;
	},

	reset: function() {
		this.__expectationBehaviorList = new Array();
		this.__actualBehaviorList = new Array();
		this.__discrepancy = null;
	},

	/**
		@returns {Discrepancy}
	*/
	discrepancy: function() {
		return this.__discrepancy; 
	},

	/**
		@private
	*/
	__matchArguments: function(expectedArgs, actualArgs) {
		var expectedArray = this.__convertArgumentsToArray(expectedArgs);
		var actualArray = this.__convertArgumentsToArray(actualArgs);
		return ArgumentMatcher.matches(expectedArray, actualArray);
	},

	/**
		@private
	*/
	__convertArgumentsToArray: function(args) {
		var convertedArguments = new Array();

		for(var i = 0; i < args.length; i++) {
			convertedArguments[i] = args[i];	
		}

		return convertedArguments;
	},
	
	/**
		@private
	*/
	__expectationBehaviorList: null,

	/**
		@private
	*/
	__actualBehaviorList: null,

	/**
		@private
	*/
	__discrepancy: null,

	/**
		@private
	*/
	__argumentMatcher: null
}

function InvocationBehavior(caller, method, methodArguments) {
	this.caller = caller;
	this.method = method;
	this.methodArguments = methodArguments;
}

function TypeOf(type) {
	if(typeof(type) != 'function')
		throw new Error("Can only take constructors");

	this.type = type;
}

/**
	@returns {TypeOf}
*/
TypeOf.isA = function(type) { return new TypeOf(type); };

ArgumentMatcher = {

	/**
		@returns {Boolean}
	*/
	matches: function(expected, actual) {
		return this.__delegateMatching(expected, actual);
	},

	/**
		@private
	*/
	__delegateMatching: function(expected, actual) {
		if( expected == null ) {
			return this.__match( expected, actual );
		} 
		else if( expected.constructor == TypeOf ) {
			return this.__match(expected.type, actual.constructor);
		} 
		else if( expected.constructor == Array ) {
			return this.__matchArrays(expected, actual);
		} 
		else {
			return this.__match(expected, actual);
		}
	}, 

	/**
		@private
	*/
	__match: function(expected, actual) {
		return ( expected == actual );
	},

	/**
		@private
	*/
	__matchArrays: function(expected, actual) {
		if ( actual == null) 
			return false;

		if( actual.constructor != Array)
			return false;

		if( expected.length != actual.length )
			return false;

		for(var i = 0; i < expected.length; i++ ) {
			if( !this.__delegateMatching(expected[i], actual[i]) )
				return false;
		}

		return true;
	}
}

function Discrepancy(message, behavior) {
	if(behavior.constructor != InvocationBehavior)
		throw new Error("The behavior can only be an InvocationBehavior object");

	this.message = message;
	this.behavior = behavior;
}

ArgumentFormatter = {

	/**
		@returns {String}
	*/	
	format: function(args) {
		var formattedArgs = "";
		for(var i = 0; i < args.length; i++) {
			if( args[i] == null ) {
				formattedArgs += ( formattedArgs == "" ) ? "null" : ", " + "null"; 
			}
			else if( args[i].constructor == TypeOf || typeof(args[i]) == "function") {
				var func = ( args[i].constructor == TypeOf ) ? args[i].type : args[i];
				formattedArgs += ( formattedArgs == "" ) ? this.__formatFunction(func) : ", " + this.__formatFunction(func); 
			}
			else if( typeof(args[i]) == "string" ) {
				formattedArgs += ( formattedArgs == "" ) ? "\"" + args[i].toString() + "\"" : ", \"" + args[i].toString() + "\""
			}
			else if( args[i].constructor == Array ) {
				formattedArgs += ( formattedArgs == "" ) ? "[" + this.format(args[i]) + "]" : ", [" + this.format(args[i]) + "]";
			}
			else {
				formattedArgs += ( formattedArgs == "" ) ? args[i].toString() : ", " + args[i].toString();
			}
		}
		return formattedArgs;
	},

	/**
		@private
	*/
	__formatFunction: function(func) {
		// Manual checking is done for internal/native functions
		// since Safari will not display them correctly 
		// for the intended regex parsing.

		if(func == Array) {
			return "Array";
		} else if(func == Date) {
			return "Date";
		} else if(func == Object) {
			return "Object";
		} else if(func == String) {
			return "String";
		} else if(func == Function) {
			return "Function";
		} else if(func == RegExp) {
			return "RegExp";
		} else if(func == Error) {
			return "Error";
		} else if(func == Number) {
			return "Number";
		} else if(func == Boolean) {
			return "Boolean";
		}
	
		var formattedFunc = func.toString().match(/function (\w+)/);

		return ( formattedFunc ==  null ) ? "{{Closure}}" : formattedFunc[1];
	}

}	
